﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using DotNetRepository.Tools;

namespace DotNetRepository
{
    public class EntityCollection<TEntity> : IEntityCollection<TEntity> where TEntity : class, IEntity
    {
        private readonly HashSet<Guid> _entityIDs = new HashSet<Guid>();
        private readonly Dictionary<Guid,TEntity> _unSavedEntities = new Dictionary<Guid,TEntity>();
        private readonly HashSet<TEntity> _entitiesToBeDeleted = new HashSet<TEntity>();

        private readonly Expression<Func<TEntity, bool>> _predicateExpression;
        private readonly Func<TEntity,bool> _predicate;
        private readonly Action<TEntity> _bind;
        private readonly Action<TEntity> _unbind;
        private bool _collectionValuesInitialized;
        private IEntity _container;
        private readonly bool _deleteOnRemove;

        public EntityCollection() 
            : this ( 
            new EntityCollectionArgs<TEntity> { ForeignKey = new StackTrace().GetFrame(1).GetMethod().ReflectedType.Name + "ID"}
        )
        {}

        public EntityCollection(string foreignKey)
            : this( new EntityCollectionArgs<TEntity> { ForeignKey = foreignKey } )
        { }

        public EntityCollection(EntityCollectionArgs<TEntity> args)
        {
            if (args.Container != null)
            {
                _container = args.Container;
            }
            else if (String.IsNullOrEmpty(args.ForeignKey))
            {
                args.ForeignKey = GetParentContainerType().Name + "ID";
            }
            
            if (args.Association != null)
            {
                _predicateExpression = args.Association;
            }
            else if (!String.IsNullOrEmpty(args.ForeignKey) || _container != null)
            {
                if (String.IsNullOrEmpty(args.ForeignKey))
                    args.ForeignKey = _container.GetType().Name + "ID";

                if (! typeof(TEntity).ContainsFieldOrProperty(args.ForeignKey))
                    throw new Exception("Foreign Key " + args.ForeignKey + " does not exist in type " + typeof (TEntity).Name);
                
                _predicateExpression = entity => ReflectionPredicate(entity, args.ForeignKey);
            }
            else
            {
                throw new Exception(
                    "Unable to initialize entity collection.  Missing foreign key or association expression.");
            }

            if (args.SetForeignKey == null && !String.IsNullOrEmpty(args.ForeignKey))
            {
                args.SetForeignKey = (entity, container) => SetForeignKey(entity, container, args.ForeignKey);
            }
            _predicate = _predicateExpression.Compile();

            _bind = args.Bind ?? (entity => args.SetForeignKey(entity, _container));
            _unbind = args.UnBind ?? (entity => args.SetForeignKey(entity, null));
            _deleteOnRemove = args.DeleteOnRemove;
        }

        public static Type GetParentContainerType()
        {
            var stackTrace = new StackTrace();
            try
            {
                // Start at frame 2 (frame 0 is this method, frame 1 is the method that called this)
                for (var i = 2; i < stackTrace.FrameCount; i++)
                {
                    var targetType = stackTrace.GetFrame(i).GetMethod().ReflectedType;
                    if (targetType.GetInterfaces().Contains(typeof(IEntity)))
                        return targetType;
                }
                throw new Exception("Could not derive container type.");
            }
            catch (Exception)
            {
                throw new Exception("Could not derive container type.");
            }
        }

        private bool ReflectionPredicate(TEntity entity, string foreignKey)
        {
            return _container.ID == (Guid) typeof (TEntity).GetValue(foreignKey, entity);
        }

        private static void SetForeignKey(TEntity entity, IEntity container, string foreignKey)
        {
            try
            {
                var containerID = container == null ? Guid.Empty : container.ID;
                typeof(TEntity).SetValue(foreignKey, entity, containerID);
            }
            catch (Exception ex)
            {
                var verb = container == null ? "unbind" : "bind";
                throw new Exception(
                    "Unable to " + verb + " " + typeof (TEntity).Name + " using foreign key " + foreignKey + ".  " +
                    ex.Message, ex);
            }
            
        }

        private void InitializeCollectionValues()
        {
            if (_collectionValuesInitialized) return;
            // If _container is new, there is no need to check the database for child references
            if (_container == null || _container.Version == 0)
            {
                _collectionValuesInitialized = true;
                return;
            }

            var tempList = Repository<TEntity>.Where(_predicateExpression);

            foreach(var deletion in _entitiesToBeDeleted)
            {
                tempList.Remove(deletion);
            }

            foreach(var item in tempList)
            {
                _entityIDs.Add(item.ID);
            }

            _collectionValuesInitialized = true;
        }

        public void Initialize(object parentContainer)
        {
            Initialize(parentContainer as IEntity);
        }

        public void Initialize(IEntity container)
        {
            if (Initialized) return;
            
            _container = container;

            // Bind all entities (since they couldn't have been properly bound previously)
            foreach (var entity in _unSavedEntities.Values)
            {
                _bind(entity);

                if (!_predicate(entity))
                    throw new Exception("Item was not successfully bound to the collection!");      
            }
            
            // Move existing (not new) entities out of the unsaved entity list
            var existingCachedEntities = _unSavedEntities.Values.Where(entity => entity.Version != 0 && Repository<TEntity>.IsCached(entity.ID)).ToArray();
            foreach (var entity in existingCachedEntities)
            {
                _unSavedEntities.Remove(entity.ID);
            }

            // Unbind removed items
            if (!_deleteOnRemove)
            {
                foreach (var item in _entitiesToBeDeleted)
                {
                    _unbind(item);
                }
            }
        }

        public void SubmitChanges()
        {
            if (!Initialized)
                throw new Exception("Changes can not be saved because the entity collection has not been initialized!");

            // Since this object is being saved, the _container must have already been saved.
            // If the _container version is 1, then this is a new entity, and does not need to be initialized.
            if (_container.Version == 1)
                _collectionValuesInitialized = true;

            var entitiesToRemove = new HashSet<Guid>();
            foreach (var id in _entityIDs)
            {
                // If this object isn't cached there are no changes to persist
                if (!Repository<TEntity>.IsCached(id)) continue;

                var entity = Repository<TEntity>.GetByID(id);

                Repository<TEntity>.Save(entity, this);

                if (!_predicate(entity))
                    entitiesToRemove.Add(entity.ID);
            }
            _entityIDs.ExceptWith(entitiesToRemove);
            
            foreach (var addition in _unSavedEntities.Values)
            {
                Repository<TEntity>.Save(addition, this);
            }
            _unSavedEntities.Clear();

            foreach (var deletion in _entitiesToBeDeleted)
            {
                if (_deleteOnRemove)
                    Repository<TEntity>.Delete(deletion, this);
                else
                    Repository<TEntity>.Save(deletion, this);
            }
            _entitiesToBeDeleted.Clear();
        }

        public void RevertChanges()
        {
            // Remove all unsavedEntities from the list of IDs
            _entityIDs.ExceptWith(_unSavedEntities.Keys);
            _unSavedEntities.Clear();

            // Replace the deleted entities
            foreach(var entity in _entitiesToBeDeleted)
            {
                _entityIDs.Add(entity.ID);
            }

            foreach(var entityID in _entityIDs)
            {
                var entity = Repository<TEntity>.GetCachedByID(entityID);
                if (entity == null) continue;
                Repository<TEntity>.Revert(entity);
            }
        }

        public void ClearAndSubmit()
        {
            Clear();
            SubmitChanges();
        }

        public void OnEntityUpdated(IEntity entityUpdated)
        {
            var entity = entityUpdated as TEntity;

            if (entity == null) return;
            
            // Remove the entity from the _unSavedEntities list (if it is contained in the list) 
            // since it has just been persisted to the database
            _unSavedEntities.Remove(entity.ID);
            
            if (Contains(entity))
            {
                // If the entity still meets the requirements of the _predicate, do nothing
                if (_predicate(entity)) return;

                // It no longer meets the requirements, so remove it
                _entityIDs.Remove(entity.ID);
                _entitiesToBeDeleted.Remove(entity);

            } else
            {
                // If the entity still doesn't meet the requirements of the _predicate, do nothing
                if (!_predicate(entity)) return;

                // It now meets the requirements, so add it
                _entityIDs.Add(entity.ID);
            }
        }

        public void OnEntityDeleted(Guid entityID)
        {
            if (_entityIDs.Contains(entityID)){
                _entityIDs.Remove(entityID);
                _unSavedEntities.Remove(entityID);
            } else
            {
                TEntity entityToRemove = _entitiesToBeDeleted.FirstOrDefault(entity => entity.ID == entityID);
                if (entityToRemove != null)
                    _entitiesToBeDeleted.Remove(entityToRemove);
            }
        }

        #region ICollection<TEntity> Members

        public TEntity this [Guid id]
        {
            get
            {
                if (!_entityIDs.Contains(id)) return null;
                return _unSavedEntities.ContainsKey(id)
                           ?
                               _unSavedEntities[id]
                           : Repository<TEntity>.GetByID(id);
            }
        }

        public void Add(TEntity entity)
        {
            if (_entityIDs.Contains(entity.ID)) return;

            if (_container != null)
            {
                _bind(entity);

                if (!_predicate(entity))
                    throw new ApplicationException("Item was not successfully bound to the collection!");                
            }

            // If this is a new object (not yet cached) then add to items to be created 
            if (entity.Version == 0 || !Repository<TEntity>.IsCached(entity.ID))
                _unSavedEntities.Add(entity.ID, entity);
            
            _entityIDs.Add(entity.ID);

            if (EntityAddedEvent != null)
                EntityAddedEvent(entity);
        }

        public bool Initialized
        {
            get { return _container != null; }
        }

        

        public void Clear()
        {
            foreach(var entityID in _entityIDs)
            {
                var entity = this[entityID];
                
                if (!_deleteOnRemove && _container != null) _unbind(entity);
                
                _entitiesToBeDeleted.Add(entity);
            }

            _unSavedEntities.Clear();
            _entityIDs.Clear();
        }

        public bool Contains(TEntity item)
        {
            InitializeCollectionValues();
            return _entityIDs.Contains(item.ID);
        }

        public bool Contains(Guid id)
        {
            InitializeCollectionValues();
            return _entityIDs.Contains(id);
        }

        public bool ContainsAny(IEnumerable<TEntity> items)
        {
            InitializeCollectionValues();
            return items.Any(item => _entityIDs.Contains(item.ID));
        }

        public bool ContainsAny(IEnumerable<Guid> ids)
        {
            InitializeCollectionValues();
            return ids.Any(id => _entityIDs.Contains(id));
        }

        public bool ContainsAll(IEnumerable<Guid> ids)
        {
            InitializeCollectionValues();
            return ids.All(id => _entityIDs.Contains(id));
        }

        public bool ContainsAll(IEnumerable<TEntity> items)
        {
            InitializeCollectionValues();
            return items.All(item => _entityIDs.Contains(item.ID));
        }

        public event EntityAddedEventHandler<TEntity> EntityAddedEvent;
        public event EntityRemovedEventHandler<TEntity> EntityRemovedEvent;

        public void CopyTo(TEntity[] array, int arrayIndex)
        {
            InitializeCollectionValues();

            var tempObjects = _unSavedEntities.Values.ToList();
            
            if (Initialized)
                tempObjects.AddRange(Repository<TEntity>.GetByIDs(_entityIDs));
            
            tempObjects.CopyTo(array, arrayIndex);
        }

        public int RecordCount
        {
            get { return Count; }
        }

        public int Count
        {
            get
            {
                InitializeCollectionValues();
                return _entityIDs.Count;
            }
        }
        public bool IsReadOnly
        {
            get { return false; }
        }

        public bool Remove(TEntity entity)
        {
            if (!Contains(entity))
                return false;

            if (_unSavedEntities.ContainsKey(entity.ID))
                _unSavedEntities.Remove(entity.ID);

            _entityIDs.Remove(entity.ID);
            _entitiesToBeDeleted.Add(entity);
            if (!_deleteOnRemove && Initialized) _unbind(entity);
            if (EntityRemovedEvent != null)
                EntityRemovedEvent(entity);
            return true;
        }

        public bool Remove(Guid id)
        {
            return Contains(id) && Remove(this[id]);
        }

        #endregion

        #region IEnumerable<TEntity> Members

        public IEnumerator<TEntity> GetEnumerator()
        {
            InitializeCollectionValues();
            return _entityIDs.Select(id => _unSavedEntities.ContainsKey(id) ? _unSavedEntities[id] : Repository<TEntity>.GetByID(id)).GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            InitializeCollectionValues();
            return _entityIDs.Select(id => _unSavedEntities.ContainsKey(id) ? _unSavedEntities[id] : Repository<TEntity>.GetByID(id)).GetEnumerator();
        }

        #endregion
    }
}
